import { Response } from "../Response"
import { StatusLine } from "./StatusLine"
import http from "@ohos.net.http"
import { headersContentLength } from "../utils/Utils"
import { CookieJar, NO_COOKIES } from "../CookieJar"
import { HttpUrl } from "../HttpUrl"
import { Headers } from "../Headers"
import { Cookie } from "../Cookie"
import { Challenge } from "../Challenge"
import { List } from "@kit.ArkTS"

/**
 * Returns true if the response headers and status indicate that this response has a (possibly
 * 0-length) body. See RFC 7231.
 */
export function promisesBody(response: Response): boolean {
  // HEAD requests never yield a body regardless of the response headers.
  if (response.request.method == "HEAD") {
    return false
  }

  let responseCode = response.code
  if ((responseCode < StatusLine.HTTP_CONTINUE || responseCode >= 200) &&
    responseCode != http.ResponseCode.NO_CONTENT &&
    responseCode != http.ResponseCode.NOT_MODIFIED) {
    return true
  }

  // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
  // response is malformed. For best compatibility, we honor the headers.
  if (headersContentLength(response) != -1 || response.header("Transfer-Encoding")?.toLowerCase() === "chunked"){
    return true
  }
  return false
}

export function receiveHeaders(cookiejar: CookieJar, url: HttpUrl, headers: Headers) {
  if (cookiejar === NO_COOKIES) return

  let cookies = Cookie.parseAll(url, headers)
  if (cookies.isEmpty()) return

  cookiejar.saveFromResponse(url, cookies)
}

/**
 * Parse RFC 7235 challenges. This is awkward because we need to look ahead to know how to
 * interpret a token.
 *
 * For example, the first line has a parameter name/value pair and the second line has a single
 * token68:
 *
 * ```
 * WWW-Authenticate: Digest foo=bar
 * WWW-Authenticate: Digest foo=
 * ```
 *
 * Similarly, the first line has one challenge and the second line has two challenges:
 *
 * ```
 * WWW-Authenticate: Digest ,foo=bar
 * WWW-Authenticate: Digest ,foo
 * ```
 */
export function parseChallenges(headers: Headers, headerName: string): List<Challenge> {
  let result = new List<Challenge>()
  for (let h = 0; h < headers.size; h++){
    if (headerName.toLowerCase() === headers.name(h).toLowerCase()){
      let header = headers.value(h)
      try {
        readChallengeHeader(header, result)
      } catch (e) {
        console.warn("Unable to parse challenge", JSON.stringify(e))
      }
    }
  }
  return result
}

/** Parse a single challenge header value (the bytes after the colon). */
function readChallengeHeader(header: string, result: List<Challenge>) {
  let i = 0
  const length = header.length

  const exhausted = () => i >= length
  const peekChar = () => exhausted() ? '' : header.charAt(i)

  function skipCommasAndWhitespace(): boolean {
    let commaFound = false
    while (!exhausted()) {
      const c = header.charAt(i)
      if (c === ',') {
        i++
        commaFound = true
      } else if (c === ' ' || c === '\t') {
        i++
      } else {
        break
      }
    }
    return commaFound
  }

  function skipAll(ch: string): number {
    let count = 0
    while (!exhausted() && header.charAt(i) === ch) {
      count++
      i++
    }
    return count
  }

  const separators = '()<>@,;:\\"/[]?={} \t'
  function isTokenChar(ch: string): boolean {
    if (!ch) return false
    const code = ch.charCodeAt(0)
    if (code <= 31 || code >= 127) return false
    if (separators.indexOf(ch) !== -1) return false
    return true
  }

  function readToken(): string | null {
    const start = i
    while (!exhausted() && isTokenChar(header.charAt(i))) {
      i++
    }
    if (i === start) return null
    return header.substring(start, i)
  }

  function startsWith(ch: string): boolean {
    return !exhausted() && header.charAt(i) === ch
  }

  function readQuotedString(): string | null {
    if (!startsWith('"')) return null
    i++ // consume '"'
    let sb = ''
    while (!exhausted()) {
      const c = header.charAt(i)
      i++
      if (c === '"') {
        return sb
      }
      if (c === '\\') {
        if (exhausted()) return null
        sb += header.charAt(i)
        i++
      } else {
        sb += c
      }
    }
    return null
  }

  let peek: string | null = null

  while (true) {
    if (peek === null || peek === undefined) {
      skipCommasAndWhitespace()
      peek = readToken()
      if (peek === null || peek === undefined) return
    }

    const schemeName = peek

    const commaPrefixed = skipCommasAndWhitespace()
    peek = readToken()
    if (peek === null || peek === undefined) {
      if (!exhausted()) return
      // challenge with no params
      result.add(new Challenge(schemeName, new Map<string, string>()))
      return
    }

    let eqCount = skipAll('=')
    const commaSuffixed = skipCommasAndWhitespace()

    // token68: no '=' following the token68 (or exhausted)
    if (!commaPrefixed && (commaSuffixed || exhausted())) {
      const token68 = peek + '='.repeat(eqCount)
      const params = new Map<string | null, string>()
      params.set(null, token68)
      result.add(new Challenge(schemeName, params))
      peek = null
      continue
    }

    // series of parameter name=value pairs
    const parameters = new Map<string | null, string>()
    eqCount += skipAll('=')
    while (true) {
      if (peek === null || peek === undefined) {
        peek = readToken()
        if (skipCommasAndWhitespace()) break // we peeked a scheme name followed by ','
        eqCount = skipAll('=')
      }
      if (eqCount === 0) break // we peeked a scheme name
      if (eqCount > 1) return // unexpected '='
      if (skipCommasAndWhitespace()) return // unexpected ','

      const parameterValue = startsWith('"') ? readQuotedString() : readToken()
      if (parameterValue === null || parameterValue === undefined) return // expected a value

      // set parameter
      parameters.set(peek, parameterValue)
      peek = null
      // duplicate parameter is an error
      // Map.set returns the map; to detect duplicates we check has before set
      // but for performance we kept simple and assume unique; if duplicate, return
      // (we can check and return)
      // advance and expect comma or end
      if (!skipCommasAndWhitespace() && !exhausted()) return
    }
    // convert parameters (Map) to Challenge
    result.add(new Challenge(schemeName, parameters))
  }
}